package com.mysoft.aiot.plugin.feign;

import com.google.common.base.Strings;
import com.mysoft.aiot.plugin.feign.model.FeignClientClass;
import com.mysoft.aiot.plugin.feign.model.FeignMethod;
import com.mysoft.aiot.plugin.feign.model.FeignMethodArgument;
import com.mysoft.aiot.plugin.feign.util.Clazz;
import com.mysoft.aiot.plugin.feign.util.Constants;
import com.mysoft.aiot.plugin.feign.util.DynamicClasspath;
import com.mysoft.aiot.plugin.feign.util.SpringControllerUtil;
import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.stream.Collectors;

/**
 * FeignClientScanner
 *
 */
public class FeignClientScanner {

  private final DynamicClasspath dynamicClasspath;

  public final Class<Annotation> controller;
  public final Class<Annotation> restController;
  public final Class<Annotation> deleteMapping;
  public final Class<Annotation> getMapping;
  public final Class<Annotation> pathVariable;
  public final Class<Annotation> postMapping;
  public final Class<Annotation> putMapping;
  public final Class<Annotation> requestBody;
  public final Class<Annotation> requestMapping;
  public final Class<Annotation> requestParam;
  public final Class<Annotation> requestMethod;

  public FeignClientScanner(DynamicClasspath dynamicClasspath) {
    this.dynamicClasspath = dynamicClasspath;

    this.dynamicClasspath.getClassLoader();

    controller = this.dynamicClasspath.loadAnnotationClazz(Constants.CONTROLLER_NAME);
    restController =
        this.dynamicClasspath.loadAnnotationClazz(Constants.REST_CONTROLLER_NAME);
    deleteMapping =
        this.dynamicClasspath.loadAnnotationClazz(Constants.DELETE_MAPPING_NAME);
    getMapping =
        this.dynamicClasspath.loadAnnotationClazz(Constants.GET_MAPPING_NAME);
    pathVariable =
        this.dynamicClasspath.loadAnnotationClazz(Constants.PATH_VARIABLE_NAME);
    postMapping =
        this.dynamicClasspath.loadAnnotationClazz(Constants.POST_MAPPING_NAME);
    putMapping =
        this.dynamicClasspath.loadAnnotationClazz(Constants.PUT_MAPPING_NAME);
    requestBody =
        this.dynamicClasspath.loadAnnotationClazz(Constants.REQUEST_BODY_NAME);
    requestMapping =
        this.dynamicClasspath.loadAnnotationClazz(Constants.REQUEST_MAPPING_NAME);
    requestParam =
        this.dynamicClasspath.loadAnnotationClazz(Constants.REQUEST_PARAM_NAME);
    requestMethod =
        this.dynamicClasspath.loadAnnotationClazz(Constants.REQUEST_METHOD_NAME);
  }

  public List<FeignClientClass> scan(String scanPackage) throws IOException {

    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Thread.currentThread().setContextClassLoader(this.dynamicClasspath.getClassLoader());

    List<String> clazzList = Clazz.getClazzName(
        scanPackage, true, this.dynamicClasspath.getClassLoader());
    JavaProjectBuilder javaProjectBuilder = new JavaProjectBuilder();
    javaProjectBuilder.addSourceTree(new File(Environment.mavenProject.getBasedir() + "/src/main/java/" + Environment.scanPackage.replace('.', '/')));
    Map<String, JavaClass> classMap = javaProjectBuilder.getClasses().stream().collect(Collectors.toMap(x -> x.getFullyQualifiedName(), x -> x));

    List<FeignClientClass> list = new ArrayList<>();

    for (String clazzName : clazzList) {

      // 反射获取 Controller 类
      try {

        Class<?> clazz = Class.forName(clazzName, false, this.dynamicClasspath.getClassLoader());

        // 判断是否是一个 Controller
        Annotation controller = clazz.getDeclaredAnnotation(this.controller);

        if (controller == null) {
          Annotation restController = clazz.getDeclaredAnnotation(this.restController);
          if (restController == null) {
            continue;
          }
        }

        FeignClientClass feignClientClass = resolveFeignClientClass(clazz);
        if (feignClientClass == null) {
          continue;
        }

        if (classMap.containsKey(clazzName)) {
          JavaClass javaClass = classMap.get(clazzName);
          String comment = javaClass.getComment();
          if (Strings.isNullOrEmpty(comment)) {
            feignClientClass.setComment(feignClientClass.getFeignClassName());
          } else {
            feignClientClass.setComment(comment);
          }

          Map<String, JavaMethod> methodMap = javaClass.getMethods().stream().collect(Collectors.toMap(x -> x.getName(), x -> x, (x, y) -> x));
          for (FeignMethod fm : feignClientClass.getMethods()) {
            if (methodMap.containsKey(fm.getName())) {
              JavaMethod javaMethod = methodMap.get(fm.getName());
              String methodComment = javaMethod.getComment();
              if (Strings.isNullOrEmpty(methodComment)) {
                fm.setComment(fm.getName());
              } else {
                fm.setComment(methodComment);
              }

              List<String> tags = new ArrayList<>();
              for (DocletTag tag : javaMethod.getTags()) {
                tags.add(tag.getName() + " " + tag.getValue());
              }
              fm.setTags(tags);
            }
          }
        }

        list.add(feignClientClass);

      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      } finally {
        Thread.currentThread().setContextClassLoader(classLoader);
      }
    }

    return list;
  }

  private FeignClientClass resolveFeignClientClass(Class<?> clazz) {

    Annotation requestMapping = clazz.getAnnotation(this.requestMapping);

    // 创建一个FeignClientClass
    FeignClientClass feignClientClass = new FeignClientClass();
    feignClientClass.setFeignPackage(Environment.outputPackage);

    feignClientClass.setHost("${" + Environment.urlPropertyName + "}");

    // 获取类名
    String clazzSimpleName = clazz.getSimpleName();

    // 获取feign接口名
    String feignName = clazzSimpleName.replaceAll("Controller", "");
    // 设置feign类名
    feignClientClass.setFeignClassName(feignName + "Feign");

    // 获取 url
    if (requestMapping != null) {
      String url = SpringControllerUtil.resolveUrlFromAnnotation(requestMapping);
      feignClientClass.setUrl(url);
    } else {
      feignClientClass.setUrl("");
    }

    // 获取所有方法
    Method[] methods = clazz.getMethods();

    // 保存这个 feign 下面的所有接口方法
    List<FeignMethod> feignMethodList = new ArrayList<>();

    Set<String> modelClasses = new HashSet<>();
    modelClasses.add("org.springframework.cloud.openfeign.FeignClient");
    modelClasses.add("org.springframework.context.annotation.Lazy");
    modelClasses.add("org.springframework.web.bind.annotation.RequestMapping");
    modelClasses.add("org.springframework.web.bind.annotation.RequestMethod");

    for (Method method : methods) {
      FeignMethod feignMethod = resolveFeignMethodFromJavaMethod(method);
      if (feignMethod != null) {
        feignMethodList.add(feignMethod);
        modelClasses.addAll(feignMethod.getModelClasses());
        for (FeignMethodArgument argument : feignMethod.getArguments()) {
          modelClasses.addAll(argument.getModelClasses());
        }
      }
    }

    if (requestMapping == null && feignMethodList.isEmpty()) {
      // 类和方法都没有声明接口
      return null;
    }

    feignClientClass.setModelClassNames(modelClasses.stream()
            .sorted()
            .collect(Collectors.toList()));

    feignMethodList.sort(Comparator.comparing(FeignMethod::getName));
    feignClientClass.setMethods(feignMethodList);

    return feignClientClass;
  }

  private FeignMethod resolveFeignMethodFromJavaMethod(Method method) {

    Annotation requestMapping = method.getAnnotation(this.requestMapping);

    String url = null;
    String httpMethod = null;

    if (requestMapping == null) {
      if (method.getAnnotation(this.getMapping) != null) {
        Annotation getMapping = method.getAnnotation(this.getMapping);
        url = SpringControllerUtil.resolveUrlFromAnnotation(getMapping);
        httpMethod = "GET";
      } else if (method.getAnnotation(this.postMapping) != null) {
        Annotation postMapping = method.getAnnotation(this.postMapping);
        url = SpringControllerUtil.resolveUrlFromAnnotation(postMapping);
        httpMethod = "POST";
      } else if (method.getAnnotation(this.putMapping) != null) {
        Annotation putMapping = method.getAnnotation(this.putMapping);
        url = SpringControllerUtil.resolveUrlFromAnnotation(putMapping);
        httpMethod = "PUT";
      } else if (method.getAnnotation(deleteMapping) != null) {
        Annotation deleteMapping = method.getAnnotation(this.deleteMapping);
        url = SpringControllerUtil.resolveUrlFromAnnotation(deleteMapping);
        httpMethod = "DELETE";
      }
    } else {
      url = SpringControllerUtil.resolveUrlFromAnnotation(requestMapping);
      httpMethod = SpringControllerUtil.resolveHttpMethodFromRequestMapping(requestMapping);
    }

    if (url == null) {
      return null;
    }

    FeignMethod feignMethod = new FeignMethod();
    feignMethod.setUrl(url);
    feignMethod.setHttpMethod(httpMethod);

    feignMethod.setName(method.getName());

    Type genericReturnType = method.getGenericReturnType();

    feignMethod.setReturnType(
        genericReturnType.getTypeName().replaceAll("([a-z0-9]+\\.)*", ""));

    getModelClassFromType(genericReturnType, feignMethod.getModelClasses());

    // 解析方法参数
    List<FeignMethodArgument> arguments = new ArrayList<>();

    Parameter[] parameters = method.getParameters();

    for (Parameter parameter : parameters) {
      FeignMethodArgument feignMethodArgument = resolveFeignMethodArgumentFromParameter(parameter);
      arguments.add(feignMethodArgument);
    }

    feignMethod.setArguments(arguments);

    return feignMethod;
  }

  private FeignMethodArgument resolveFeignMethodArgumentFromParameter(Parameter parameter) {

    FeignMethodArgument argument = new FeignMethodArgument();

    String name = parameter.getName();
    Class<?> type = parameter.getType();
    Type parameterizedType = parameter.getParameterizedType();

    Annotation[] annotations = parameter.getAnnotations();
    List<String> as = new ArrayList<>();
    for (Annotation annotation : annotations) {
      Class<? extends Annotation> clazz = annotation.getClass();
      Class requestParam = this.dynamicClasspath.loadClazz(Constants.REQUEST_PARAM_NAME);
      Class requestBody = this.dynamicClasspath.loadClazz(Constants.REQUEST_BODY_NAME);
      Class pathVariable = this.dynamicClasspath.loadClazz(Constants.PATH_VARIABLE_NAME);
      if (requestParam.isAssignableFrom(clazz)) {
        try {
          List<String> paramExpressions = new ArrayList<>();
          // value 字段
          Method valueMethod = clazz.getMethod("value");
          String value = valueMethod.invoke(annotation).toString();
          if (!Strings.isNullOrEmpty(value)) {
            paramExpressions.add(valueMethod.getName() + " = \"" + value + "\"");
          }

          // required 字段
          Method requiredMethod = clazz.getMethod("required");
          boolean required = Boolean.parseBoolean(requiredMethod.invoke(annotation).toString());
          if (!required) {
            paramExpressions.add(requiredMethod.getName() + " = " + required);
          }

          if (paramExpressions.isEmpty()) {
            as.add(annotation.annotationType().getSimpleName());
          } else {
            as.add(annotation.annotationType().getSimpleName() + "(" + String.join(", ", paramExpressions) + ")");
          }
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
          as.add(annotation.annotationType().getSimpleName());
        }
        getModelClassFromType(requestParam, argument.getModelClasses());
      } else if (requestBody.isAssignableFrom(clazz)) {
        as.add(annotation.annotationType().getSimpleName());
        getModelClassFromType(requestBody, argument.getModelClasses());
      } else if (pathVariable.isAssignableFrom(clazz)) {
        as.add(annotation.annotationType().getSimpleName());
        getModelClassFromType(pathVariable, argument.getModelClasses());
      }
    }

    argument.setName(name);
    argument.setType(parameterizedType.getTypeName().replaceAll("([a-z0-9]+\\.)*", ""));
    argument.setAnnotations(as);

    getModelClassFromType(parameterizedType, argument.getModelClasses());
    getModelClassFromType(type, argument.getModelClasses());

    return argument;
  }

  private void getModelClassFromType(Type type, Set<String> list) {
    if (type instanceof ParameterizedType) {
      getModelClassFromType((ParameterizedType) type, list);
    } else if (type instanceof GenericArrayType) {
      getModelClassFromType((GenericArrayType) type, list);
    } else if (!(type instanceof WildcardType || type instanceof TypeVariable)) {
      if (type instanceof Class && Clazz.isBaseType((Class)type)) {
        return;
      } else {
        String typeName = type.getTypeName();
        if (!typeName.startsWith(Environment.outputPackage)) {
          list.add(typeName);
        }
      }
    }
  }

  private void getModelClassFromType(ParameterizedType type, Set<String> list) {
    Type rawType = type.getRawType();

    list.add(rawType.getTypeName());
    Type[] actualTypeArguments = type.getActualTypeArguments();
    for (Type actualTypeArgument : actualTypeArguments) {
      if (actualTypeArgument instanceof ParameterizedType) {
        getModelClassFromType((ParameterizedType) actualTypeArgument, list);
      } else {
        String argClassName = actualTypeArgument.getTypeName();
        if (!list.contains(argClassName)) {
          list.add(argClassName);
        }
      }
    }
  }

  private void getModelClassFromType(GenericArrayType type, Set<String> list) {
    getModelClassFromType(type.getGenericComponentType(), list);
  }
}
